import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * Authors - Oren Bracha, Rony Lahav & Yehuda Eterman
 * This class represents the DB. Every action/access of DB is performed through this class. From reading through
 * updating to deleteing.
 */
public class DataBase implements Serializable, DBInterface{
	private static DataBase dbInstance;   // Singleton instance
	
	//private CTOR
	private DataBase(){}
	
	// Singleton access
	public static DataBase getInstance(){
		if (dbInstance == null){
			dbInstance = new DataBase(); 
		}
		return dbInstance;
	}

	/** This method gets the number of peers that have the complete picture of a specific peer 
	 * and divide it by the parameter N and returns the result
	 */
	public float findProbabiltyForPictureUpdate(String peerName){
		DBTuple tuple = tuplesMap.get(peerName);
		if(tuple == null){
			return -1;
		}
		int countOfPeers = tuple.getCountOfPeersWithNewPic();
		return (float)countOfPeers / (float)proj350.getNParameter();
	}
	
	/**
	 * This method gets a peername and returns the list of all the peers that this peername is in
	 * their list (has their picture) 
	 */
	public String[] findPeersThatHavePeerInPicSet(String peerName){
		String result = "";
		Collection<DBTuple> c = tuplesMap.values();
		
		for (DBTuple tuple : c) {
			if(tuple.checkIfPeerInSet(peerName)){
				result += tuple.getPeerName() + ",";
			}
		}

		return result.split(",");
	}
	
	/**
	 * This method returns the number of peers that the parameter peer name is in their lisr t (has their picture)
	 */
	public synchronized int findHowManyPeersHaveNewPic(String peerName){
		DBTuple tuple = tuplesMap.get(peerName);
		if(tuple == null){
			return -1;
		}
		return tuple.getCountOfPeersWithNewPic();
	}
	
	/**
	 * When I know for sure that a peer has a specific peer's picture (completely) I can add it to the
	 * specific peer's list
	 */
	public synchronized void addPeerThatHasNewPic(String peerName, String peerWithNewpicture){
		DBTuple tuple = tuplesMap.get(peerName);
		if(tuple == null){
			return;
		}
		tuple.addToSetOfPeersWithNewPic(peerWithNewpicture);
	}
	
	/**
	 * This method returns the next fragment number I want to ask for a specific picture(spcific peer)
	 */
	public synchronized String getNumberOfNextPicSegment(String peerName){
		DBTuple tuple = tuplesMap.get(peerName);
		if(tuple == null){
			return null;
		}
		// Get the number of fragments I already have for this peer and add 1 - this is the next fragment
		int nextSegment = tuple.getIncompletePicture().size() + 1;
		
		return Integer.toString(nextSegment);
	}
	
	/**
	 * 
	 * Returns for a specific peer if it's picture received completely
	 */
	public synchronized boolean isPictureCompletelyReceived(String peerName){
		DBTuple tuple = tuplesMap.get(peerName);
		
		// Check if the amount of fragments already received is all of the picture
		return (Integer.parseInt(tuple.getPicSize())*2<=tuple.getIncompletePicture().size()*proj350.getMSS());		
	}
	
	/**
	 * This method gets a peername and a list of picture fragments and add the fragments to the peer's list of
	 * fragments
	 */
	public synchronized void addSegmentsToPicture(String peerName, String[] segments){
		DBTuple tuple = tuplesMap.get(peerName);
		if(tuple == null){
			// peer does'nt exist in my DB
			return;
		}
		List<String> incompletePicture = tuple.getIncompletePicture();
		if(incompletePicture == null){
			// Incomplete picture doesnt exist
			incompletePicture = new ArrayList<String>();
			tuple.setIncompletePicture(incompletePicture);
		}
		for (int i = 0; i < segments.length; i++){
			// Add the fragments to the peer's fragments list
			incompletePicture.add(segments[i]);
		}
	}
	
	/**
	 * return all tuples in DB
	 */
	public synchronized SortedMap<String, DBTuple> getAllTuples(){
		return tuplesMap;
	}
	
	/**
	 * awake all tuples in DB
	 */
	public synchronized void awakenAllPeers(){
		for (DBTuple tuple : tuplesMap.values()) {
			tuple.setStatus("1");
		}		
	}
	
	/**
	 * Return all tuples in DB that are OnLine (but the requesting peer) 
	 */
	public synchronized SortedMap<String, DBTuple> getAllTuplesForUpdate(){
		SortedMap<String, DBTuple> newMap = new TreeMap<String, DBTuple>();
	
		Collection<DBTuple> c = tuplesMap.values();
		
		for (DBTuple tuple : c) {
			// Check if the checked peer is online and that it is not me...
			if (tuple.getStatus() && tuple.getPeerName().compareTo(proj350.getMyPeerName()) != 0) {
				newMap.put(tuple.getPeerName(), tuple);
			}
		}
		return newMap;
	}
	
	/**
	 * This method load a DB object from a the file determined by filename parameter.
	 * It returns true if the load was successful or false otherwise
	 */
	public synchronized boolean LoadDBFromFile(String fileName)
	{
		try
		{
			// Read the DB object from the file and load it
			ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName));
			if (ois != null){
				dbInstance = (DataBase)ois.readObject();
				return true;
			}
		}
		// Any exception will fail the load
		catch(IOException e1)
		{
			return false;
		}
		catch(ClassNotFoundException e2)
		{
			return false;
		}
		return false;
	}
	
	/**
	 * This method saves the DB object to a the file determined by filename parameter.
	 * It returns true if the save was successful or false otherwise
	 */
	public synchronized boolean SaveDBToFile(String fileName)
	{
		try
		{
			// Save the DB object to the file
			ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName));
			oos.writeObject((Object)(dbInstance));
			
			return true;
		}
		// An exception will fail the save
		catch(IOException e)
		{
			return false;
		}
	}
	
	/**
	 * @param PeerName - peer to start from
	 * @return all tuples in DB from the peerName parameter and further
	 */
	public synchronized SortedMap<String, DBTuple> getTuplesFromPeer(String peerName){
		SortedMap<String, DBTuple> headMap = tuplesMap.tailMap(peerName);
		headMap.remove(peerName);
		return headMap;
	}
	
	/**
	 * @param PeerName - peer to end with
	 * @return all tuples in DB until the peerName parameter
	 */
	public synchronized SortedMap<String, DBTuple> getTuplesUntilPeer(String peerName){
		SortedMap<String, DBTuple> headMap = tuplesMap.headMap(peerName);
		headMap.remove(peerName);
		return headMap;
	}
	
	/**
	 * This method gets a "from" peer and a "to" peer and returns all of the peers in the range that have
	 * later commit time than the parameter dbVersion
	 */
	public synchronized SortedMap<String, DBTuple> getRangeOfPeers(String peer1, String peer2, String dbVersion){
		SortedMap<String, DBTuple> smap = null;
		if (peer1.compareTo("0")==0){
			smap = getTuplesUntilPeer(peer2);
		}
		else if (peer2.compareTo("all")==0){
			smap = getTuplesFromPeer(peer2);
		}
		else{
			smap = tuplesMap.tailMap(peer1).headMap(peer2);
			smap.remove(peer1);
			smap.remove(peer2);
		}
		
		SortedMap<String, DBTuple> returnValue = new TreeMap<String, DBTuple>();
		long requestedVersion = DateStringConverter.getMSFromStringMessageFormat(dbVersion);
		//go over all peers and find the peer with later DB Version
		for (DBTuple tuple : tuplesMap.values()) {
			long tupleVersion = DateStringConverter.getMSFromStringMessageFormat(tuple.getPeerCommitTime());
			if (requestedVersion < tupleVersion) {
				returnValue.put(tuple.getPeerName(), tuple);
			}
		}
		
		return returnValue;
	}
	
	/**
	 * This returns a map of all of the tuples in the DB that have commit time later than the dbVersion parameter
	 */
	public synchronized SortedMap<String, DBTuple> getTuplesByDbVersion(String dbVersion) {
		if (dbVersion.compareTo("0")==0){
			dbVersion = DateStringConverter.getZeroDateAsString();
		}
		
		long requestedVersion = DateStringConverter.getMSFromStringMessageFormat(dbVersion);
		SortedMap<String, DBTuple> returnValue = new TreeMap<String, DBTuple>();
		//go over all peers and find the peer with later DB Version
		for (DBTuple tuple : tuplesMap.values()) {
			long tupleVersion = DateStringConverter.getMSFromStringMessageFormat(tuple.getPeerCommitTime());
			
			if (requestedVersion < tupleVersion) {
				returnValue.put(tuple.getPeerName(), tuple);
			}
		}
		return returnValue;
	}

	/**
	 * 
	 * Update the dbVersion of the parameter peer to the current time
	 */
	public synchronized void updateDBVersionToCurrent(String peerName)
	{
		DBTuple Tuple = tuplesMap.get(peerName);
		Tuple.setDbVersion(DateStringConverter.timeCurrentForMessages());
	}
	
	/**
	 * update an Tuple in the DB
	 */
	public synchronized void updateTuple(DBTuple tuple) {
		// Remove the old Tuple, if it exists
		tuplesMap.remove(tuple.getPeerName());

		// Insert the tuple
		tuplesMap.put(tuple.getPeerName(), tuple);
	}
	
	/**
	 * for the given tuple, check if it is newer then the tuple in the 
	 * DB and update if necessary
	 */
	public synchronized void updateTupleIfRelevant(DBTuple newTuple) {
		//get the current tuple from the DB
		DBTuple oldTuple = tuplesMap.get(newTuple.getPeerName());
		String zeroDateAsString = DateStringConverter.getZeroDateAsString();
		//if tuple exists
		if (oldTuple != null) {
			long oldTime = DateStringConverter.getMSFromStringMessageFormat(oldTuple.getPeerVersion());
			long newTime = DateStringConverter.getMSFromStringMessageFormat(newTuple.getPeerVersion());
			
			//check which is older and update accordingly
			if (newTime > oldTime) {
				//updating data for picture
				newTuple.setPeerCommitTime(oldTuple.getPeerCommitTime());
				newTuple.setIncompletePicture(oldTuple.getIncompletePicture());
				newTuple.setPeersWithNewPic(oldTuple.getPeersWithNewPic());
				
				if(DateStringConverter.compare2Dates(newTuple.getPicVersion(), oldTuple.getPicVersion()) > 0){
					//set new times and collections for the picture
					newTuple.setPeerCommitTime(zeroDateAsString);
					newTuple.newSetOfPeersWithNewPic();
					newTuple.addToSetOfPeersWithNewPic(newTuple.getPeerName());
					newTuple.setIncompletePicture(new ArrayList<String>());
				}
				else{
					//put commit time to now
					if (newTuple.getPeerCommitTime().compareTo(zeroDateAsString) != 0){
						newTuple.setPeerCommitTime(DateStringConverter.timeCurrentForMessages());
					}
				}
		
				// Insert the tuple
				tuplesMap.put(newTuple.getPeerName(), newTuple);
			}
			else
			{
				//updating DB version
				long oldDbVersion = DateStringConverter.getMSFromStringMessageFormat(oldTuple.getDbVersion());
				long newDbVersion = DateStringConverter.getMSFromStringMessageFormat(newTuple.getDbVersion());
				if (newDbVersion > oldDbVersion){
					oldTuple.setDbVersion(newTuple.getDbVersion());
				}
			}
		} 
		else {
			// Insert the tuple if the peer is missing
			newTuple.setPeerCommitTime(zeroDateAsString);
			newTuple.newSetOfPeersWithNewPic();
			newTuple.addToSetOfPeersWithNewPic(newTuple.getPeerName());
			newTuple.setIncompletePicture(new ArrayList<String>());
			tuplesMap.put(newTuple.getPeerName(), newTuple);
		}
	}

	/**
	 * 
	 * @return all the tuples in the DB - in TEXT
	 */
	public String asString() {
		String sb = "";
		for (DBTuple tuple : tuplesMap.values()) {
			sb += (MessageCenter.newline + "_______________________" + MessageCenter.newline) + (tuple.toString());
		}

		return sb;
	}

	/**
	 * Returns true if the DB is empty, else otherwise
	 */
	public synchronized boolean isDBEmpty() {
		return tuplesMap.isEmpty();
	}
	
	/**
	 * Cleans the DB
	 */
	public synchronized void cleanDB(){
		tuplesMap.clear();
	}
	
	/**
	 * setting the last connection time to now
	 */
	public synchronized void setLastConnectionTime(String peerName){
		DBTuple tuple = tuplesMap.get(peerName);
		if(tuple == null){
			return;
		}
		tuple.setLastConnectionTime(DateStringConverter.timeCurrentForMessages());
	}
	
	/**
	 * mark peer as off line
	 * @param peerName
	 */
	public synchronized void setPeerOffline(String peerName){
		DBTuple tuple = tuplesMap.get(peerName);
		if(tuple == null){
			return;
		}
		tuple.setStatus("0");
	}
	
	/**
	 * mark peer as on line
	 * @param peerName
	 */
	public synchronized void setPeerOnline(String peerName){
		DBTuple tuple = tuplesMap.get(peerName);
		if(tuple == null){
			return;
		}
		tuple.setStatus("1");
	}
	
	// return the DB Tuple of the peer
	public synchronized DBTuple findTuple(String PeerName){
		return tuplesMap.get(PeerName);
	}
	
	
	private static final long serialVersionUID = -2487765432167654329L;
	
	/**
	 * creates a copy of the current DB to read from (avoids duplications when dealing with requestentry messages)
	 * @return a copy of the Data Base
	 */
	public synchronized DBCopy createCopy(){
		SortedMap<String, DBTuple> newMap = new TreeMap<String, DBTuple>();
		Collection<DBTuple> c = tuplesMap.values();
		for (DBTuple tuple : c) {
			newMap.put(tuple.getPeerName(), tuple.clone());
		}
		
		DBCopy dbCopy = new DBCopy(newMap);
		return dbCopy;
	}
	
	//we save all of the Peer Tuples in this map
	private SortedMap<String, DBTuple> tuplesMap = new TreeMap<String, DBTuple>();
	

}
